'use strict'

const fs = require('fs')
const path = require('path')
const process = require('process')

const inquirer = require('inquirer')
const fse = require('fs-extra')
const semver = require('semver')
const userHome = require('user-home')
const kebabCase = require('kebab-case')
const ejs = require('ejs')
const glob = require('glob')

const Package = require('@lenovo-cli/package')
const Command = require('@lenovo-cli/command')
const log = require('@lenovo-cli/log')
const { spinnerStart, sleep, spawnExecSync } = require('@lenovo-cli/utils')

const request = require('@lenovo-cli/request')

const {
  TYPE_PROJECT,
  TYPE_COMPONENT,
  TEMPLATE_TYPE_NORMAL,
  TEMPLATE_TYPE_CUSTOM,
  WHITE_COMMAND,
} = require('./const')

class InitCommand extends Command {
  init() {
    this.projectName = this._argv[0] || ''
    this.force = !!this._cmd._optionValues.force
    log.verbose('projectName', this.projectName)
    log.verbose('force', this.force)
  }

  async exec() {
    try {
      // init的业务逻辑
      // 准备阶段
      const projectInfo = await this.prepare()
      if (projectInfo) {
        // 下载模版
        log.verbose('projectInfo', projectInfo)
        this.projectInfo = projectInfo
        await this.downloadTemplate()
        // 安装模板
        await this.installTemplate()
      }
    } catch (e) {
      log.error(e.message)
      if (process.env.LOG_LEVEL === 'verbose') {
        console.log(e)
      }
    }
  }

  async prepare() {
    // 判断项目模版是否存在
    const template = await this.getProjectTemplate()
    if (!template || template.length === 0) {
      throw new Error('项目模版不存在')
    }
    this.template = template // 将template报存在实例中
    const loaclPath = process.cwd() // 获取当前文件所在目录 （path.resolve('.) 也可以）
    // 判断当前目录是否为空，不为空时
    if (!this.isDirEmpty(loaclPath)) {
      let ifContinue = false
      if (!this.force) {
        // 不为空，是否继续创建
        ifContinue = (
          await inquirer.prompt({
            type: 'confirm',
            name: 'ifContinue',
            default: false,
            message: '当前文件夹不为空，是否继续创建项目？',
          })
        ).ifContinue
        if (!ifContinue) {
          return
        }
      }
      if (ifContinue || this.force) {
        // 清空前二次确认
        const { confirmDelete } = await inquirer.prompt({
          type: 'confirm',
          name: 'confirmDelete',
          default: false,
          message: '是否确认清空当前目录下的文件？',
        })
        if (confirmDelete) {
          // 清空当前目录
          fse.emptyDirSync(loaclPath)
        }
      }
    }

    // 当前目录为空，获取要安装的工程信息
    return this.getInfos()
  }

  async getInfos() {
    function valiProjectName(name) {
      return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(
        name
      )
    }
    let projectInfo = {}
    // 选择创建项目或者组件
    const { type } = await inquirer.prompt({
      type: 'list',
      name: 'type',
      message: '请选择初始化类型',
      default: TYPE_PROJECT,
      choices: [
        {
          name: '项目',
          value: TYPE_PROJECT,
        },
        {
          name: '组件',
          value: TYPE_COMPONENT,
        },
      ],
    })
    const title = type === TYPE_PROJECT ? '项目' : '组件'
    // 按照类型将template中的多个模板进行过滤
    this.template = this.template.filter(t => t.tag.includes(type))
    // 创建inquirer流程
    const arrPrompt = []

    const projectNamePrompt = {
      type: 'input',
      message: `请输入${title}名称`,
      name: 'projectName',
      default: '',
      validate: function (v) {
        const done = this.async()
        setTimeout(function () {
          // 输入的首字符必须为英文字符&&尾字符必须为英文或者数字且不能为字符&&字符仅允许'-_'
          //Tag \w=a-zA-Z0-9_
          if (!valiProjectName(v)) {
            done(
              '请输入正确的项目名称：\n * 输入的首字符必须为英文字符 \n * 尾字符必须为英文或者数字且不能为字符 \n * 字符仅允许 - 和 _ '
            )
            return
          }
          done(null, true)
        })
      },
      filter: function (v) {
        return v
      },
    }
    if (!valiProjectName(this.projectName)) {
      // 当init时输入的项目名字校验未通过时，重新进入输入名字流程
      arrPrompt.push(projectNamePrompt)
    } else {
      // 通过时直接跳过输入名字流程，并将名字赋值
      projectInfo.projectName = this.projectName
    }

    arrPrompt.push(
      {
        type: 'input',
        name: 'version',
        message: `请输入${title}版本号`,
        default: '1.0.0',
        validate: function (v) {
          const done = this.async()
          setTimeout(function () {
            if (!semver.valid(v)) {
              done('输入的版本号格式不正确')
              return
            }
            done(null, true)
          })
        },
        filter: function (v) {
          if (!!semver.valid(v)) {
            return semver.valid(v)
          }
          return v
        },
      },
      {
        type: 'list',
        name: 'projectTemplate',
        message: `请选择${title}模版`,
        choices: this.createTemplateChoice(),
      }
    )

    if (type === TYPE_COMPONENT) {
      // 组件描述信息
      arrPrompt.push({
        type: 'input',
        name: 'componentDescription',
        message: '请输入描述信息',
        default: '',
        validate: function (v) {
          const done = this.async()
          setTimeout(function () {
            if (!v) {
              done('描述信息不能为空！')
              return
            }
            done(null, true)
          })
        },
      })
    }

    const project = await inquirer.prompt(arrPrompt)
    projectInfo = {
      ...projectInfo,
      type,
      ...project,
    }

    // 转换驼峰命名 AaBb -> aa-bb
    if (projectInfo.projectName) {
      projectInfo.packageName = kebabCase(projectInfo.projectName).replace(
        /-/,
        ''
      )
    }
    if (projectInfo.componentDescription) {
      projectInfo.description = projectInfo.componentDescription
    }

    return projectInfo
  }

  async downloadTemplate() {
    const { projectTemplate } = this.projectInfo

    // templateInfo - 从templates中筛选出当前下载的模版信息
    const templateInfo = this.template.find(
      item => item.npmPkgName === projectTemplate
    )
    const targetPath = path.resolve(userHome, '.lenovo-cli', 'template')
    const storeDir = path.resolve(
      userHome,
      '.lenovo-cli',
      'template',
      'node_modules'
    )
    const { npmPkgName, version } = templateInfo
    this.templateInfo = templateInfo
    const templateNpm = new Package({
      targetPath,
      storeDir,
      packageName: npmPkgName,
      packageVersion: version,
    })
    if (!(await templateNpm.exists())) {
      const spinner = spinnerStart('正在下载...')
      await sleep()
      try {
        await templateNpm.install()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)
        if (await templateNpm.exists()) {
          log.success('下载成功！')
          this.templateNpm = templateNpm // 将下载的npm包储存在全局
        }
      }
    } else {
      const spinner = spinnerStart('正在更新...')
      await sleep()
      try {
        await templateNpm.update()
      } catch (e) {
        throw e
      } finally {
        spinner.stop(true)
        if (await templateNpm.exists()) {
          log.success('更新成功！')
          this.templateNpm = templateNpm
        }
      }
    }
  }

  async installTemplate() {
    if (this.templateInfo) {
      if (!this.templateInfo.type) {
        // type不存在时，设置默认为normal
        this.templateInfo.type = TEMPLATE_TYPE_NORMAL
      }
      if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
        // 标准安装
        await this.installNormalTemplate()
      } else if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
        // 自定义安装
        await this.installCustomTemplate()
      } else {
        throw new Error('项目模版类型无法识别!')
      }
    } else {
      throw new Error('项目模版信息不存在!')
    }
  }
  async installNormalTemplate() {
    log.verbose('templateNpm', this.templateNpm)
    // 拷贝模版代码至当前目录
    let spinner = spinnerStart('正在安装模版...')
    await sleep()
    try {
      const templatePath = path.resolve(
        this.templateNpm.cacheFilePath, // cacheFilePath是当前的包的缓存路径， 再拼接/template, 为缓存目录中安装包的目录
        'template'
      )
      const targetPath = process.cwd() // 当前执行的脚手架的目录
      // 通过fse确保目录存在（不存在会自动创建)
      fse.ensureDirSync(templatePath)
      fse.emptyDirSync(targetPath)
      fse.copySync(templatePath, targetPath)
    } catch (e) {
      throw e
    } finally {
      spinner.stop(true)
      log.success('模版安装成功')
    }

    // 进行ejs动态模版渲染
    await this.ejsRender()

    // // 依赖安装
    const { installCommand, startCommand } = this.templateInfo
    let installRet // 安装进程变量
    if (installCommand) {
      const installCmd = installCommand.split(' ')
      const cmd = this.checkCommand(installCmd[0])
      const args = installCmd.slice(1)
      installRet = await spawnExecSync(cmd, args, {
        stdio: 'inherit',
        cwd: process.cwd(),
      })
    }

    if (installRet !== 0) {
      throw new Error('依赖安装失败！')
    }
    // 启动命令执行
    if (startCommand) {
      const startCmd = startCommand.split(' ')
      const cmd = startCmd[0]
      const args = startCmd.slice(1)
      await spawnExecSync(cmd, args, {
        stdio: 'inherit',
        cwd: process.cwd(),
      })
    }
  }

  ejsRender() {
    const tempIgnore = this.templateInfo.ignore || []
    const ignoreFiles = ['node_modules/**', ...tempIgnore] // public下的html中的模版渲染要忽略，因为webpack一般会配置html中的link的href
    return new Promise((resolve, reject) => {
      glob(
        '**',
        {
          cwd: process.cwd(), // 便利当前目录
          ignore: ignoreFiles,
          nodir: true, // 不需要渲染文件夹，去掉文件夹
        },
        (err, files) => {
          if (err) {
            reject(err)
          }
          // 获取模版文件后进行处理
          Promise.all(
            files.map(file => {
              const filePath = path.join(process.cwd(), file)
              return new Promise((res, rej) => {
                ejs.renderFile(
                  filePath, // 文件路径
                  this.projectInfo, // 要渲染的数据
                  {}, // 可选配置默认为空
                  (err, data) => {
                    if (err) {
                      rej(err)
                    } else {
                      // 需要将渲染结果重新写入文件
                      fse.writeFileSync(filePath, data)
                      res(data)
                    }
                  }
                )
              })
            })
          )
            .then(() => {
              resolve()
            })
            .catch(err => {
              reject(err)
            })
        }
      )
    })
  }

  async installCustomTemplate() {
    log.info('安装自定义模版...')
    if (await this.templateNpm.exists()) {
      const rootFile = this.templateNpm.getRootFilePath()
      if (fs.existsSync(rootFile)) {
        log.notice('执行自定义模版...')
        const templatePath = path.resolve(
          this.templateNpm.cacheFilePath,
          'template'
        )
        const options = {
          templateInfo: this.templateInfo,
          projectInfo: this.projectInfo,
          sourcePath: templatePath,
          targetPath: process.cwd(),
        }
        const code = `require('${rootFile}')(${JSON.stringify(options)})`
        log.verbose('code', code)
        await spawnExecSync('node', ['-e', code], {
          stdio: 'inherit',
          cwd: process.cwd(),
        })
        log.success('自定义模版安装成功！')
      } else {
        throw new Error('入口文件不存在！')
      }
    }
  }

  createTemplateChoice() {
    return this.template.map(item => ({
      value: item.npmPkgName,
      name: item.name,
    }))
  }

  isDirEmpty(loaclPath) {
    let fileList = fs.readdirSync(loaclPath)
    fileList = fileList.filter(
      // 文件过滤逻辑
      file => !file.startsWith('.') && ['node_modules'].indexOf(file) < 0
    )
    return !fileList || fileList.length <= 0
  }

  getProjectTemplate() {
    return request({
      url: '/project/template',
    })
  }

  checkCommand(cmd) {
    if (WHITE_COMMAND.indexOf(cmd) >= 0) {
      return cmd
    }
    throw new Error(`错误的npm包安装命令: ${cmd}，请使用npm、cnpm、yarn.`)
  }
}

module.exports.InitCommand = InitCommand
module.exports = argv => new InitCommand(argv)
